/*
后台用户登陆验证用

有以下几种:

  - [X] 能 **读取** 用户信息: 自动更新 Token, 保留时间稍长

  - [X] 能 **添加/修改/删除** 用户信息: 非自动更新 Token , 保留时间更短

Debug:
  redis 的连接池会导致 获取到的结果是上一次未读取到的结果 OR 未完成的连接 ? NOTE: 是否应该禁用 pool ?
*/
package route

import (
	"context"
	"crypto/rand"
	"errors"
	"github.com/golang/protobuf/proto"
	"github.com/gomodule/redigo/redis"
	"golang.org/x/crypto/scrypt"
	"net/http"
	mysqlMode "route/mysql"
	protobuf "route/protobuf"
	redisMode "route/redis"
	"strconv"
	"table"
	"time"
	"utils"
)

var (
	ErrIrregularSign = errors.New("签名不规则")
	ErrUnableParse   = errors.New("签名无法解析")

	ErrAccountFailed = errors.New("帐户申请失败")
	ErrLoginInFailed = errors.New("登陆失败，请稍后再试")
	ErrInvalidMsg    = errors.New("用户名或密码无效")
	ErrServerError   = errors.New("服务器错误")
)

type debugCtx struct{ value byte }

type mdlCtx struct{ value byte }

var (
	Test      = &debugCtx{0}
	TestRedis = &debugCtx{1}
	TestFile  = &debugCtx{2}

	UserCtx = &mdlCtx{0}
	RecCtx  = &mdlCtx{1}
)

// AdminLoginUp 注册
func AdminLoginUp(w http.ResponseWriter, req *http.Request) {
	req.ParseForm()
	name, jobNum, passwd, salt := req.FormValue("name"), req.FormValue("job_num"), req.FormValue("password"), make([]byte, 64)
	rand.Read(salt)

	// 密码记录
	N, ra, p, keyLen := ScryptSlice[0], ScryptSlice[1], ScryptSlice[2], ScryptSlice[3]
	encpasswd, err := scrypt.Key([]byte(passwd), salt, N, ra, p, keyLen)
	if err != nil {
		*req = *req.WithContext(context.WithValue(req.Context(), Test, "60$"+err.Error()+"$"+ErrAccountFailed.Error())) // DEBUG
		http.Error(w, ErrAccountFailed.Error(), http.StatusBadRequest)
		return
	}

	now := time.Now()
	// insert mysql
	_, err = mysqlMode.Exec("INSERT INTO admin_user"+
		" (username, password, salt, id_person, date_create, date_modify)"+
		" VALUES (?, ?, ?, (SELECT p.id FROM person p WHERE p.num_job = ?), ?, ?)",
		name, encpasswd, salt, jobNum, now.Unix(), now.Unix()) // username 唯一
	// _, err = CustomStmts.GetStmt("insert", "admin_user", 0).Exec(name, encpasswd, salt)
	if err != nil {
		*req = *req.WithContext(context.WithValue(req.Context(), Test, "68$"+err.Error()+"$"+ErrAccountFailed.Error())) // DEBUG
		http.Error(w, ErrAccountFailed.Error(), http.StatusBadRequest)
		return
	}
	w.WriteHeader(http.StatusOK)
}

// AdminLoginInByForm 登陆, 获取 token
func AdminLoginInByForm(w http.ResponseWriter, req *http.Request) {
	req.ParseForm()
	jobNum := req.FormValue("job_num")
	// name := req.FormValue("name")
	passwd := req.FormValue("password")
	// token 不可用, 认证 用户/密码
	rows, err := mysqlMode.Query(
		"SELECT u.id, u.password, u.salt FROM admin_user u"+
			" INNER JOIN (SELECT p.id FROM person p WHERE num_job = ?) p ON p.id = u.id_person ",
		jobNum)
	// rows, err := CustomStmts.GetStmt("select", "admin_user", 0).Query(name)
	if err != nil {
		// 内部错误
		*req = *req.WithContext(context.WithValue(req.Context(), Test, "85$"+err.Error()+"$"+ErrLoginInFailed.Error())) // DEBUG
		http.Error(w, ErrLoginInFailed.Error(), http.StatusServiceUnavailable)
		return
	}

	if !rows.Next() {
		// 认证失败
		*req = *req.WithContext(context.WithValue(req.Context(), Test, "92$"+ErrInvalidMsg.Error())) // DEBUG
		http.Error(w, ErrInvalidMsg.Error(), http.StatusBadRequest)
		return
	}

	user := &table.AdminUser{}
	rows.Scan(&user.ID, &user.Password, &user.Salt)

	// 密码认证
	N, r, p, keyLen := ScryptSlice[0], ScryptSlice[1], ScryptSlice[2], ScryptSlice[3]
	encPasswd, err := scrypt.Key([]byte(passwd), user.Salt, N, r, p, keyLen)
	// bytes.Compare 由于 bytes 包不是作为加解密用的包, 不能做到 *任意长* 的密码比对时间 *相同*
	// 因此选择先加密编码到相同的长度(64)再做比对
	// TODO: 通过 [异或]来比对密码各个字节, 结果的相加不为 0 来判断密码是否相符
	if err != nil || utils.XorCompare(encPasswd, user.Password) != 0 {
		// 认证失败
		*req = *req.WithContext(context.WithValue(req.Context(), Test, "105$"+err.Error()+"$"+ErrInvalidMsg.Error())) // DEBUG
		http.Error(w, ErrInvalidMsg.Error(), http.StatusBadRequest)
		return
	}

	// 认证成功
	conn := redisMode.Get()
	defer conn.Close()

	// BUG: unexpected type for ByteSlices, got type int64(1)
	// byteID := make([]byte, 8)
	// binary.BigEndian.PutUint64(byteID, uint64(user.ID))

	// byteID := []byte(strconv.Itoa(user.ID))

	// 动态 Token 自动更新
	{
		expire := time.Now().Add(dynamic.frontendExpire)
		signature, token, err := newToken(jobNum, expire.Unix())
		if err != nil {
			*req = *req.WithContext(context.WithValue(req.Context(), Test, "125$"+err.Error()+"$"+ErrLoginInFailed.Error())) // DEBUG
			http.Error(w, ErrLoginInFailed.Error(), http.StatusServiceUnavailable)
			return
		}

		rkt := &protobuf.Token{
			Id:     user.ID,
			User:   jobNum,
			Expire: expire.Format(TimeFormat),
		}
		s, _ := proto.Marshal(rkt)
		if err := conn.Send("SET", dynamic.redis+signature, s, "EX", dynamic.tokenExpire); err != nil {
			*req = *req.WithContext(context.WithValue(req.Context(), Test, "137$"+err.Error()+"$"+ErrServerError.Error())) // DEBUG
			http.Error(w, ErrServerError.Error(), http.StatusBadRequest)
			return
		}
		conn.Flush()
		// go func() {
		// conn.Do("EXPIRE", dynamicName.redis+signature, dynamicName.tokenExpire)
		// }()
		w.Header().Set(dynamic.header, token) // Header
		http.SetCookie(w, &http.Cookie{       // Cookie
			Name:     dynamic.header,
			Value:    token,
			HttpOnly: true,
			Secure:   true,
			MaxAge:   int(dynamic.frontendExpire.Seconds()),
		})
	}

	// 静态 Token
	{
		expire := time.Now().Add(static.frontendExpire)
		signature, token, err := newToken(jobNum, expire.Unix())
		if err != nil {
			*req = *req.WithContext(context.WithValue(req.Context(), Test, "153$"+err.Error()+"$"+ErrLoginInFailed.Error())) // DEBUG
			http.Error(w, ErrLoginInFailed.Error(), http.StatusServiceUnavailable)
			return
		}
		rtk := &protobuf.Token{
			Id:     user.ID,
			User:   jobNum,
			Expire: expire.Format(TimeFormat),
		}
		s, _ := proto.Marshal(rtk)
		if _, err := conn.Do("SET", static.redis+signature, s, "EX", static.tokenExpire); err != nil {
			*req = *req.WithContext(context.WithValue(req.Context(), Test, "164$"+err.Error()+"$"+ErrLoginInFailed.Error())) // DEBUG
			http.Error(w, ErrServerError.Error(), http.StatusBadRequest)
			return
		}
		conn.Flush()
		// go func() {
		// conn.Do("EXPIRE", staticName.redis+signature, staticName.tokenExpire)
		// }()

		w.Header().Set(static.header, token)
		http.SetCookie(w, &http.Cookie{ // Cookie
			Name:     static.header,
			Value:    token,
			HttpOnly: true,
			Secure:   true,
			MaxAge:   int(static.frontendExpire.Seconds()),
		})
	}

	w.WriteHeader(http.StatusOK)
}

// AdminLoginInByJWT 登陆, 通过现有的 动态 JWT [Header Token] 验证
func AdminLoginInByJWT(w http.ResponseWriter, r *http.Request) {
	tourl := Protocol + r.Host + "/page/loginin"
	auth, _, err := splitSignatureFromHeader(r.Header.Get(dynamic.header))
	if err != nil {
		*r = *r.WithContext(context.WithValue(r.Context(), Test, "184$"+dynamic.header+"$"+r.Header.Get(dynamic.header)+"$"+err.Error())) // DEBUG
		w.Header().Set("Connection", "close")
		http.Redirect(w, r, tourl, http.StatusTemporaryRedirect) // 临时重定向
		return
	}
	conn := redisMode.Get() // 从 redis 获取 连接 conn
	defer conn.Close()
	// 从 redis 验证 token 并且拿 具体的信息
	s0, err := conn.Do("GET", dynamic.redis+auth)
	s, err := redis.Bytes(s0, err)
	if err != nil {
		*r = *r.WithContext(context.WithValue(r.Context(), Test, "195$"+err.Error()+ErrLoginInFailed.Error())) // DEBUG
		*r = *r.WithContext(context.WithValue(r.Context(), TestRedis, s))                                      // DEBUG
		http.Error(w, ErrLoginInFailed.Error(), http.StatusServiceUnavailable)
		// log.Println(s, s0, auth, dynamicName.redis+auth)
		return
	}
	rtk := &protobuf.Token{}
	err = proto.Unmarshal(s, rtk)
	if err != nil {
		http.Error(w, ErrLoginInFailed.Error(), http.StatusServiceUnavailable)
		*r = *r.WithContext(context.WithValue(r.Context(), Test, "205$"+err.Error()+ErrLoginInFailed.Error())) // DEBUG
		return
	}
	expire, err := time.Parse(TimeFormat, rtk.Expire) // expire
	if err != nil {
		// redis 的 token 无效
		*r = *r.WithContext(context.WithValue(r.Context(), Test, "211$"+err.Error())) // DEBUG
		w.Header().Set("Connection", "close")
		http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
		return
	}

	// token 存在
	if time.Now().Unix() > expire.Unix() {
		// toke 过期
		newExpire := time.Now().Add(dynamic.frontendExpire)
		signature, token, err := newToken(string(rtk.User), newExpire.Unix())
		if err != nil {
			*r = *r.WithContext(context.WithValue(r.Context(), Test, "223$"+err.Error())) // DEBUG
			// return "", http.StatusServiceUnavailable, ErrLoginInFailed
			w.Header().Set("Connection", "close")
			http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
			// log.Println(s)
			return
		}

		newRtk := &protobuf.Token{
			Id:     rtk.Id,
			User:   rtk.User,
			Expire: newExpire.Format(TimeFormat),
		}
		s, _ := proto.Marshal(newRtk)
		if _, err := conn.Do("SET", dynamic.redis+signature, s, "EX", dynamic.tokenExpire); err != nil {
			*r = *r.WithContext(context.WithValue(r.Context(), Test, "238$"+err.Error()+"$"+ErrServerError.Error())) // DEBUG
			http.Error(w, ErrServerError.Error(), http.StatusBadRequest)
			return
		}
		conn.Flush()
		// go func() {
		// conn.Do("EXPIRE", dynamicName.redis+signature, dynamicName.tokenExpire)
		// }()
		w.Header().Set(dynamic.header, token)
		w.WriteHeader(http.StatusOK)
		return
	}

	// token 未过期, 继续使用
	w.Header().Set(dynamic.header, r.Header.Get(dynamic.header))
	w.WriteHeader(http.StatusOK)
	return
}

// MdlCheckDynamicToken 中间件/检测 动态 token 是否过关, 不过关重定向到登陆页面
func MdlCheckDynamicToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// // /loginin/** 和 /loginup 另外处理
		// if !strings.HasPrefix(r.RequestURI, "/api/v1/auth/loginin") && !strings.HasPrefix(r.RequestURI, "/api/v1/auth/loginup") {
		// 	next.ServeHTTP(w, r)
		// 	return
		// }

		tourl := Protocol + r.Host + "/page/loginin"
		// 验证
		token := r.Header.Get(dynamic.header)
		if token == "" {
			cookie, err := r.Cookie(dynamic.header)
			if err != nil {
				*r = *r.WithContext(context.WithValue(r.Context(), Test, "286$"+err.Error())) // DEBUG
				w.Header().Set("Connection", "close")
				http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
				return
			}
			token = cookie.Value
		}
		auth, _, err := splitSignatureFromHeader(token)
		if err != nil {
			*r = *r.WithContext(context.WithValue(r.Context(), Test, "295$"+err.Error())) // DEBUG
			w.Header().Set("Connection", "close")
			http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
			return
		}
		conn := redisMode.Get() // 从 redis 获取 连接 conn
		defer conn.Close()

		switch isExistORNotExpire, s, err := checkToken(conn, dynamic.redis, auth); isExistORNotExpire {
		case notExist:
			*r = *r.WithContext(context.WithValue(r.Context(), Test, "305$"+err.Error()+"$not exist")) // DEBUG
			http.Error(w, ErrLoginInFailed.Error(), http.StatusServiceUnavailable)
			// return

		case existButFailParse:
			*r = *r.WithContext(context.WithValue(r.Context(), Test, "310$"+err.Error()+"$exist bug failure to parse time")) // DEBUG
			w.Header().Set("Connection", "close")
			http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
			// return

		case existButExpire:
			// idR := binary.BigEndian.Uint64(s[0]) // id
			rtk := &protobuf.Token{} // redis token
			proto.Unmarshal(s, rtk)
			// idR, _ := strconv.Atoi(string(s[0]))
			// userR := s[1] // user
			newExpire := time.Now().Add(dynamic.frontendExpire)
			signature, token, err := newToken(rtk.User, newExpire.Unix())
			if err != nil {
				*r = *r.WithContext(context.WithValue(r.Context(), Test, "324$"+err.Error())) // DEBUG
				w.Header().Set("Connection", "close")
				http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
				return
			}
			newRtk := &protobuf.Token{
				Id:     rtk.Id,
				User:   rtk.User,
				Expire: newExpire.Format(TimeFormat),
			}
			newS, _ := proto.Marshal(newRtk)
			if _, err = conn.Do("SET", dynamic.redis+signature, newS, "EX", dynamic.tokenExpire); err != nil {
				*r = *r.WithContext(context.WithValue(r.Context(), Test, "336$"+err.Error()+"$"+ErrServerError.Error())) // DEBUG
				http.Error(w, ErrServerError.Error(), http.StatusBadRequest)
				return
			}
			conn.Flush()
			// go func() {
			// conn.Do("EXPIRE", dynamicName.redis+signature, dynamicName.tokenExpire)
			// }()
			w.Header().Set(dynamic.header, token) // 更新 token
			fallthrough

		case existAndNotExpire:
			// 记录下当前的操作信息
			// path, _ := mux.CurrentRoute(r).GetPathTemplate()
			// go func() {
			// 	idR, _ := strconv.Atoi(string(s[0]))
			// 	// 获得在 NewRouter() 时的路由
			// 	recordOperation(idR, path)
			// }()
			next.ServeHTTP(w, r)
		}
	})
}

// MdlCheckStaticToken 中间件/检测 静态 token 是否过关, 不过关重定向到登陆页面
func MdlCheckStaticToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 验证
		tourl := Protocol + r.Host + "/page/loginin"

		token := r.Header.Get(static.header)
		if token == "" {
			cookie, err := r.Cookie(dynamic.header)
			if err != nil {
				w.Header().Set("Connection", "close")
				http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
				return
			}
			token = cookie.Value
		}
		auth, _, err := splitSignatureFromHeader(token)
		if err != nil {
			w.Header().Set("Connection", "close")
			http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
			r = r.WithContext(context.WithValue(r.Context(), Test, "378$"+err.Error())) // DEBUG
			return
		}
		conn := redisMode.Get() // 从 redis 获取 连接 conn
		defer conn.Close()

		switch isExistORNotExpire, s, _ := checkToken(conn, static.redis, auth); isExistORNotExpire {
		case notExist:
			http.Error(w, ErrLoginInFailed.Error(), http.StatusServiceUnavailable)
			r = r.WithContext(context.WithValue(r.Context(), Test, "387$"+"not exist")) // DEBUG
			r = r.WithContext(context.WithValue(r.Context(), TestRedis, s))             // DEBUG
			return
		case existButFailParse:
			fallthrough

		case existButExpire:
			w.Header().Set("Connection", "close")
			http.Redirect(w, r, tourl, http.StatusTemporaryRedirect)
			r = r.WithContext(context.WithValue(r.Context(), Test, "396$"+"exist but ...")) // DEBUG
			r = r.WithContext(context.WithValue(r.Context(), TestRedis, s))                 // DEBUG
			return

		case existAndNotExpire:
			// 记录下当前的操作信息
			// path, _ := mux.CurrentRoute(r).GetPathTemplate()
			// go func() {
			// 	idR, _ := strconv.Atoi(string(s[0]))
			// 	// 获得在 NewRouter() 时的路由
			// 	recordOperation(idR, path)
			// }()
			idR, _ := strconv.Atoi(string(s[0]))
			r = r.WithContext(context.WithValue(r.Context(), UserCtx, int32(idR)))
			next.ServeHTTP(w, r)
		}
	})
}

const (
	notExist          = 0x00 // token 查询失败
	existButFailParse = 0x10 // token 查询成功但解析失败
	existButExpire    = 0x01 // token 查询成功但过期
	existAndNotExpire = 0x11 // token 查询成功且未过期
)
